Lua & xLua 语法学习(Unity向)

目标:一篇文吃透 Lua 语法xLua 互操作语法。不聊工程目录,只聊概念 + 语法 + 可直接运行的片段。

0. 速览:Lua 的“七件套”

  • 基础类型:nil / boolean / number / string / table / function / thread(协程)(还有 userdata,在 xLua 中常见于 C# 对象)
  • 三类运算:算术 + - * / % // ^、比较 == ~= < > <= >=、逻辑 and or not
  • 两个“黑魔法”:表(table)元表(metatable)
    表是一切组合数据的容器;元表让你给表(或 userdata)定制运算与访问规则。

1. 变量、作用域与表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
--#region 变量与作用域
local x = 10 -- local:局部变量(推荐默认都用 local)
y = 20 -- 无 local:全局变量(容易污染命名空间,不推荐)
do
local x = 99 -- 块级作用域:do ... end 内部的 x 与外部不同
print("inner x", x) -- 99
end
print("outer x", x) -- 10
--#endregion

--#region 数字与字符串
local a, b = 7, 3
print(a / b) -- 浮点除法 2.333...
print(a // b) -- 整除 2
print(a % b) -- 取模 1
print(2 ^ 3) -- 次方 8

local s = "Lua"
print("hi " .. s) -- 字符串拼接用 ..
print(#s) -- 字符串长度(对表是“长度运算符”,见后文)
--#endregion

--#region 多重赋值与交换
local i, j = 1, 2
i, j = j, i -- 交换变量
print(i, j) -- 2 1
--#endregion

2. 函数、闭包与可变参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
--#region 函数基础
local function add(x, y) -- 定义函数(local 防止泄露到全局)
return x + y -- Lua 支持多返回值:return a, b, c
end
print(add(3, 4)) -- 7
--#endregion

--#region 闭包(捕获外部变量)
local function makeCounter() -- 工厂函数
local n = 0 -- 被捕获的“上值”(upvalue)
return function() -- 返回匿名函数
n = n + 1
return n
end
end
local nextId = makeCounter()
print(nextId(), nextId()) -- 1 2(状态被函数记住)
--#endregion

--#region 可变参数 ...
local function sum(...) -- ... 接收任意数量实参
local total = 0
for _, v in ipairs({...}) do -- 把 ... 收集到新表再遍历(简单直观)
total = total + v
end
return total
end
print(sum(1,2,3,4)) -- 10
--#endregion

--#region 冒号语法糖(面向对象风格)
local Player = {}
function Player.say(self, msg) -- 点语法:需要手动传 self
print(self.name .. ": " .. msg)
end
function Player:say2(msg) -- 冒号语法:定义时自动加 self 参数
print(self.name .. ": " .. msg)
end
local p = {name = "Lee"}
Player.say(p, "hello") -- 等价
p: say2("hi") -- 等价于 Player.say2(p, "hi")
--#endregion

3. 表(Table)最重要:从入门到专家

3.1 构造、读写与删除

1
2
3
4
5
6
7
8
9
10
11
12
13
--#region 构造器(数组式 + 字典式可混用)
local arr = {10, 20, 30} -- “数组式”下标从 1 开始
local dict = {x = 1, y = 2} -- “字典式”键是字符串
local mix = {100, a = 5, ["weird-key"] = 7, [true] = "ok"} -- 任意键
--#endregion

--#region 访问与修改
print(arr[1], arr[2]) -- 10 20
print(dict.x, dict["y"]) -- 1 2(t.k 等价 t["k"])
mix["weird-key"] = 8 -- 修改
dict.z = 3 -- 新增键
dict.x = nil -- 删除键:设为 nil 即删除
--#endregion

3.2 遍历与长度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
--#region 遍历
for i, v in ipairs(arr) do -- ipairs:连续整数键 1..n,遇到第一个 nil 停
print(i, v)
end
for k, v in pairs(mix) do -- pairs:遍历所有键(无序,不保证顺序稳定)
print(k, v)
end
--#endregion

--#region 长度运算符 #
local t = {1, 2, 3}
print(#t) -- 3
t[4] = nil -- “洞”会影响 # 运算
print(#t) -- 未定义行为(可能仍是 3,也可能 2)——不要依赖
-- 正解:自己维护长度或用 table.move/table.insert/remove 管理
--#endregion

3.3 常用库函数(table

1
2
3
4
5
6
7
8
9
10
11
12
--#region 插入/删除/拼接/排序/打包
local t = { "b", "a", "c" }
table.insert(t, 1, "z") -- 在索引 1 插入 "z":{"z","b","a","c"}
table.remove(t, 2) -- 删除索引 2:{"z","a","c"}
table.sort(t, function(lhs, rhs) -- 自定义排序(按字母升序)
return lhs < rhs
end)
print(table.concat(t, ",")) -- "a,c,z"

local packed = table.pack(1, nil, 3) -- {1,nil,3}; packed.n=3 可保存长度包括 nil
local a, b, c = table.unpack({7,8,9}) -- 解包
--#endregion

3.4 引用语义、浅拷贝与深拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
--#region 引用语义
local t1 = {a=1}
local t2 = t1 -- 赋值只是“指向同一张表”
t2.a = 9
print(t1.a) -- 9
--#endregion

--#region 浅拷贝
local function shallow_copy(src)
local dst = {}
for k, v in pairs(src) do
dst[k] = v -- 仅复制第一层,嵌套表仍是“同一个引用”
end
return dst
end
--#endregion

--#region 深拷贝(带环检测)
local function deep_copy(src, seen)
if type(src) ~= "table" then return src end
if seen and seen[src] then return seen[src] end
local dst = {}
seen = seen or {}
seen[src] = dst
for k, v in pairs(src) do
dst[deep_copy(k, seen)] = deep_copy(v, seen)
end
-- 可选:拷贝元表
return setmetatable(dst, getmetatable(src))
end
--#endregion

3.5 rawget/rawset只读表带默认值表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
--#region rawget/rawset:绕过元表的“钩子”直接读写底层存储
local t = {}
local mt = {
__index = function() return 42 end
}
setmetatable(t, mt)
print(t.someKey) -- 42(触发 __index)
print(rawget(t, "someKey")) -- nil(不触发 __index,读到真实存储)
--#endregion

--#region 只读表(访问 OK,写入报错)
local function readonly(tbl)
return setmetatable({}, {
__index = tbl, -- 读:转发到原表
__newindex = function() error("table is readonly") end, -- 写:报错
__pairs = function() return pairs(tbl) end, -- 遍历:透传
__len = function() return #tbl end
})
end
--#endregion

--#region 默认值表(访问不存在键时返回默认)
local function with_default(defaultValue)
return setmetatable({}, {
__index = function(_, _) return defaultValue end
})
end
local bag = with_default(0)
bag.apple = bag.apple + 1 -- 读不到时当 0
print(bag.apple) -- 1
--#endregion

3.6 元表(Metatable)与“像类一样用表”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
--#region 运算符重载与属性“魔法”
local vec = {x=1, y=2}
local mt = {}
mt.__add = function(a,b) return {x=a.x+b.x, y=a.y+b.y} end -- 重载 +
mt.__tostring = function(v) return ("(%d,%d)"):format(v.x,v.y) end
mt.__index = function(t,k)
if k == "len" then
return math.sqrt(t.x*t.x + t.y*t.y) -- 惰性“属性”
end
end
setmetatable(vec, mt)
print(vec + {x=3,y=4}) -- (4,6)
print(vec.len) -- 2.236...
--#endregion

--#region “类风格”构造与方法
local Class = {} -- 类表(方法定义在它上面)
Class.__index = Class -- 实例查不到键时从 Class 上找(方法)
function Class:new(name) -- 构造器
local o = {name = name} -- 实例
return setmetatable(o, Class) -- 绑定元表
end
function Class:say(msg) -- 方法
print(self.name .. ": " .. msg)
end
local c = Class:new("Alice")
c:say("yo") -- Alice: yo
--#endregion

--#region 弱表(缓存与对象池):__mode = "k" / "v" / "kv"
local cache = setmetatable({}, {__mode="v"}) -- 值是弱引用,被外部释放后会被 GC 回收
--#endregion

4. 字符串与模式匹配(正则替代品)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
--#region 常用 API:sub/find/gsub/format
local s = "Lua-XLua-Unity"
print(string.sub(s, 5, 8)) -- "XLua"
print(string.find(s, "Unity")) -- 起止位置
print(string.gsub("a,b,c", ",", "|")) -- "a|b|c", 次数
print(string.format("pos=(%0.2f,%0.2f)", 1.234, 5.678))
--#endregion

--#region 模式匹配(不完全等于正则,但够用)
local text = "id=42 user=lee"
for k, v in string.gmatch(text, "(%w+)=(%w+)") do
print(k, v) -- id 42 / user lee
end
--#endregion

5. 协程:可暂停的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
--#region 生产者-消费者示例
local function producer()
return coroutine.create(function()
for i=1,3 do
coroutine.yield(i) -- 产出一个值并挂起
end
return "done" -- 最终返回
end)
end

local co = producer()
print(coroutine.resume(co)) -- true 1
print(coroutine.resume(co)) -- true 2
print(coroutine.resume(co)) -- true 3
print(coroutine.resume(co)) -- true done
--#endregion

6. 错误与异常:error / assert / pcall / xpcall

1
2
3
4
5
6
7
8
9
10
11
12
--#region pcall:捕获错误不抛出
local function div(a,b)
if b == 0 then error("divide by zero") end
return a/b
end

local ok, res = pcall(div, 1, 0)
print(ok, res) -- false divide by zero
-- xpcall 可附带 traceback:
local ok2, res2 = xpcall(function() return div(1,0) end, debug.traceback)
print(ok2, res2)
--#endregion

7. xLua 语法要点:Lua ↔ C#

不谈工程目录,只谈 怎么在 Lua 里“像 C# 那样用类型”C# 怎么调用 Lua

7.1 在 Lua 里访问 C#(CS 命名空间)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
--#region CS 入口(静态/实例/枚举)
local CS = CS -- 简写
CS.UnityEngine.Debug.Log("[Lua] hello") -- 静态方法
local go = CS.UnityEngine.GameObject("FromLua") -- new GameObject("FromLua")
go.transform.position = CS.UnityEngine.Vector3(0,1,0) -- 值类型“整体赋回”(详见 7.3)
local KeyCode = CS.UnityEngine.KeyCode
print(KeyCode.Space) -- 枚举
--#endregion

--#region 泛型(List / Dictionary)
local ListInt = CS.System.Collections.Generic.List(CS.System.Int32) -- List<int>
local list = ListInt() -- 构造
list:Add(10); list:Add(20)
print(list:get_Count(), list:Contains(10)) -- 2 true

local DictSF = CS.System.Collections.Generic.Dictionary(CS.System.String, CS.System.Single) -- Dictionary<string,float>
local dict = DictSF()
dict:Add("atk", 12.5)
print(dict:get_Item("atk")) -- 12.5
--#endregion

--#region 委托/事件(Action/Func)
local Action = CS.System.Action
local function onQuit() CS.UnityEngine.Debug.Log("[Lua] quitting") end
-- 订阅 .NET 事件:组合委托(不同类型事件有不同 API,以下是 .NET 风格示例)
CS.UnityEngine.Application.quitting = CS.System.Delegate.Combine(
CS.UnityEngine.Application.quitting,
Action(onQuit)
)
-- 取消订阅:Delegate.Remove(...)
--#endregion

7.2 在 C# 里调用 Lua(委托/接口映射)

核心思路:把 Lua 函数“看作” C# 的委托或接口,这样 C# 端调用更类型安全、开销更低。
这需要白名单 + 代码生成[CSharpCallLua] 等),此处仅示意“怎么用”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#region C# 伪代码(重点是“语法”)
/*
Lua 侧:
Demo = {}
function Demo.add(x,y) return x+y end
function Demo.on_click(name) print(name) end
ButtonHandler = { OnClick = function(self, name) print(name) end }
*/

var table = env.Global.Get<LuaTable>("Demo"); // 取到 Lua 表
var add = table.Get<Func<int,int,int>>("add"); // 把 Lua 函数映射成 C# 委托
Debug.Log(add(3,4)); // 7
var onClick = table.Get<Action<string>>("on_click"); // 同理
onClick("Start");

var handler = env.Global.Get<IButtonHandler>("ButtonHandler"); // 把 Lua 表当作接口对象
handler.OnClick("Play");
table.Dispose(); // 用完记得释放
#endregion

7.3 值类型陷阱(Vector3 等)

  • Vector3/Color 等是 值类型(struct)。在 Lua 里读取 go.transform.position 得到的是值的副本不能直接点改
    正确做法:改临时变量,再整体赋回
1
2
3
local pos = go.transform.position  -- 取到副本
pos.y = pos.y + 1 -- 修改副本
go.transform.position = pos -- 整体赋回(这一步才生效)

7.4 数字类型与重载解析

  • Lua 的 number 在 xLua 里通常走 double。传给 C# 的 int/float 会自动转换,但签名重载有歧义时可能报错。
    解决:用显式类型构造消歧义,例如 CS.System.Single(1.0)CS.System.Int32(1)

7.5 性能与内存小抄(互操作相关)

  • 频繁调用的 Lua 函数请 缓存委托/接口,避免每次 Get<T> 与查表。
  • LuaTable/LuaFunction 用完 立刻 Dispose();否则 Lua GC 无法及时回收。
  • 跨语言传大对象用 ID/句柄 替代,另一侧再查询,减少边界拷贝。

8. 常见语法坑位清单(90% 的报错都在这)

  1. attempt to index a nil value:访问了 nil。先 print(type(x)) 看是否为 nil
  2. # 运算对“有洞的数组”不可靠:不要用 #t 统计带空洞的数组长度。
  3. 结构体属性“点改不生效”Vector3 之类要整体赋回(见 7.3)。
  4. 委托签名不匹配Action<string> 与 Lua 函数的参数个数/类型必须一致。
  5. 排序比较器写错返回值:比较函数应返回 true/false,而不是 -1/0/1

9. 实战练习(聚焦 Table 与 xLua 语法)

练习 A:实现一个“默认 0 的计数表”

1
2
3
4
5
6
7
8
9
10
11
12
13
--#region 练习 A 参考解法(逐行注释)
local function counter()
local t = {}
setmetatable(t, {
__index = function(_, _) return 0 end -- 读不到键时默认为 0
})
return t
end
local c = counter()
c.apple = c.apple + 1 -- 读不到时返回 0,再 +1
c.banana = c.banana + 2
print(c.apple, c.banana) -- 1 2
--#endregion

练习 B:只读配置表

1
2
3
4
5
6
7
8
9
10
11
12
--#region 练习 B 参考解法
local function readonly(tbl)
return setmetatable({}, {
__index = tbl,
__newindex = function() error("readonly") end,
__pairs = function() return pairs(tbl) end
})
end
local cfg = readonly({hp=100, atk=20})
print(cfg.hp) -- 100
-- cfg.hp = 200 -- 报错:readonly
--#endregion

练习 C(xLua):用 List 做桥接而不是 Lua 表

1
2
3
4
5
6
--#region 练习 C 参考解法
local ListInt = CS.System.Collections.Generic.List(CS.System.Int32)
local scores = ListInt() -- 避免把 Lua 表传到 C# 再逐一解析,直接用 .NET 容器
scores:Add(90); scores:Add(85); scores:Add(100)
print(scores:get_Count()) -- 3
--#endregion

10. 小结

  • 是 Lua 的心脏:会构造、会遍历、懂“洞”、会用 table.*、会玩元表,你就能写出优雅的 Lua。
  • xLua 语法的核心就三件事:CS 访问 + 泛型/委托写法 + 值类型赋回
  • 写法上坚持:local 默认小函数多闭包边界少拷贝用完就 Dispose()